<?php // InsightUser.php

class InsightUser extends dbObject {
	public $user_id;
	public $locale;
	public $org_id;
	public $username;
	public $refresh_token;
	public $partner_api;
	public $display_name;
	public $email;
	public $timezone;
	public $signup_date;
	public $trial_exp_date;
	public $status;
	public $google_token;
	
	// Preferences
	public $dismiss_google_domain_lookups;
	
	public static $CURRENT_USER; // static hold for the current user
	
	public function __construct($uid=null) {
		$this->dbTable = 'user';
		$this->tableKey = 'user_id';
		$this->keyType = 'hash';

		if ($uid == null && isset($_SESSION["user_id"])) {
			if (isset(InsightUser::$CURRENT_USER)) {
				foreach (get_object_vars(InsightUser::$CURRENT_USER) as $key => $val) {
					$this->$key = $val;
				}
				return;
			} else {
				$uid = $_SESSION["user_id"];
			}
		}
			
		if ($uid != null) {
			$user = $this->find($uid);
			foreach (get_object_vars($user) as $key => $val) {
				$this->$key = $val;
			}
			$_SESSION["user_id"] = $uid;
                        
		}
               
		if (isset($this->user_id) && isset($_SESSION["user_id"])) {
                    	
                    if ($this->user_id == $_SESSION["user_id"]) {
				InsightUser::$CURRENT_USER = $this;
			}
		}
	}
	
	public static function findUser($uid) {
		$user = new InsightUser();
		$user->find($uid);
		return $user;
	}
	
	public function insert() {
		parent::insert();
		
		// ensure this user is in the org set
		if (isset($this->org_id)) {
			$org = new CIOrg($this->org_id);
			$org->addUser($this->user_id);
		}
	}
	
	public function update() {
		InsightUser::insert();
	}
	
	public static function Authorized() {
		if (!isset($_SESSION["access_token"]) || !isset($_SESSION["partner_endpoint"])) {
			if (isset($_COOKIE["insight_token"])) {
				$token = urldecode($_COOKIE["insight_token"]);
				$user_id = Util::decrypt($token);

				$user = new InsightUser($user_id);
				if (isset($user->user_id) && isset($user->refresh_token) && !!Util::decrypt($user->refresh_token)) {
					$user->requestAccessToken();
				}

				// return false if there's still no info
				if (!isset($_SESSION["access_token"]) || !isset($_SESSION["partner_endpoint"])) return false;
			} else {
				return false;
			}
		}

		$user = new InsightUser();
		$user->doGlobalDescribe();
		return true;
	}
	
	public function getInstanceUrl() {
		$instanceUrl = substr($this->partner_api, 0, stripos($this->partner_api, '/', 8)); // omits trailing "/"
		return $instanceUrl;
	}
	
	public function getRecordUrl($recId) {
		$url = $this->getInstanceUrl() . '/' . $recId;
		return $url;
	}
	
	public function setInsightToken() {
		if (array_key_exists("user_id", $_SESSION)) {
			setcookie(
				$name="insight_token", $data=urlencode(Util::encrypt($_SESSION["user_id"])), // basics 
				$expire=time()+60*60*24*30, // expire in a month 
				$path="/",
				$domain=SESSION_HOST, 
				$secure=true, // Only set this cookie when the script is accessed over HTTPS 
				$httponly=true // The cookie will be made accessible only through the HTTP protocol. This means that the cookie won't be accessible by scripting languages, such as JavaScript
			);
		}
	}
	
	public function createSession($token) {
		$_SESSION["user_id"] = $this->user_id;
		$_SESSION["org_id"] = $this->org_id;
		$_SESSION["partner_endpoint"] = $this->partner_api;
		$_SESSION["refresh_token"] = $this->refresh_token;
		$_SESSION["access_token"] = $token;
	}
	
	public function getTimezone() {
		if (Util::isNullOrEmpty($this->timezone)) {
			$userInfo = Salesforce::getUserInfo($this->user_id);
			if (!Util::isNullOrEmpty($userInfo->fields->TimeZoneSidKey)) {
				$this->timezone = $userInfo->fields->TimeZoneSidKey;
				$this->update();
			} else {
				// Log a case to investigate
				Util::logCase("ERROR_GET_TIMEZONE", array("userInfo"=>$userInfo, "insightUser"=>$this));
			}
		}
	}
	
	public function requestAccessToken() {
		$this->find($this->user_id); // query from the db
		
		$unencrypted_token = Util::decrypt($this->refresh_token);
		if (!!$unencrypted_token) {
			// request the access token using the refresh token
			$refreshUrl = SFDC_REFRESH_TOKEN_URL . $unencrypted_token;
			$response = Util::callout($refreshUrl, 'POST');
			
			if ($response->status == '200' && isset($response->data->access_token)) {
				$this->createSession($response->data->access_token);
				$result = new stdClass();
					$result->success = true;
				return $result;
			} else {
				$this->refresh_token = null;
				$this->update();
				Util::logCase("ERROR_REQUEST_NEW_TOKEN", array(
					"FE_INSTANCE" => FE_INSTANCE,
					"RESPONSE" => $response, 
					"USER" => $this, 
					"COOKIES"=>$_COOKIE,
					"refreshUrl" => $refreshUrl
				));
				
				$result = new stdClass();
					$result->success = false;
					$result->error = "Error refreshing authorization token.";
				return $result;
			}
		} else {
			$result = new stdClass();
				$result->success = false;
				$result->error = "User must log in.";
			return $result;
		}

		$result = new stdClass();
			$result->success = true;
		return $result;
	}
	
	public function getPersonQuery($person) {
		$record_limit = 5;
		$fields = array('Id');
		$obj = strtolower($person->type);
		
		if (!isset($_SESSION["{$obj}_query"])) {
			$person_config = $this->getObjConfig($obj);
			foreach ($person_config as $f) {
				if (isset($f->type) && $f->type == 'special') {
					continue;
				} else if (isset($f->type) && $f->type == 'hover') {
					foreach ($f->fields as $subf) {
						$fields[] = $f->relationshipName . '.' . strtolower($subf->name);
	
						// add additional owner fields
						if (strtolower($subf->name) == 'ownerid') {
							$fields[] = $f->relationshipName . '.Owner.Name';
						}
					}
					
					// make sure the Id is included
					if (!in_array($f->relationshipName . '.id', $fields)) {
						$fields[] = $f->relationshipName . '.Id';
					}
					
					// make sure the Id is included
					if (!in_array($f->relationshipName . '.name', $fields)) {
						$fields[] = $f->relationshipName . '.Name';
					}
				} else {
					$fields[] = $f->name;
					// add additional owner fields
					if (strtolower($f->name) == 'ownerid') {
						$fields[] = 'Owner.Name';
					}
				}
			}
			
			$query = 'Select '. implode(', ', $fields) .', ';
			
			$subqs = array();
			$layout = $this->getLayout($person);
			foreach ($layout->relatedLists as $list) {
				if (!$this->hasRelatedList($obj, $list->name)) continue;
				
				$fields = array();
				foreach ($list->columns as $col) {
					$fld = str_ireplace('__c.', '__r.', $col->name);
					$fields[] = $fld;
					// add additional owner fields
					if (strtolower($col->name) == 'ownerid') {
						$fields[] = 'Owner.Name';
					}
				}
				$q = 'Select Id, '.implode(', ', $fields).' From '.$list->name;
	
				if (isset($list->sort)) {
					// special rules for these lists
					// "activity aggregate relationships only allow security evaluation for non-admin users when sorting on ActivityDate ASC, LastModifiedDate DESC for OpenActivity or ActivityDate DESC, LastModifiedDate DESC for ActivityHistory"
					if ($list->name == 'OpenActivities') {
						$q .= ' ORDER BY ActivityDate ASC, LastModifiedDate DESC';
						
					} else if ($list->name == 'ActivityHistories') {
						$q .= ' ORDER BY ActivityDate DESC, LastModifiedDate DESC';

					} else {
						if (!is_array($list->sort)) $list->sort = array($list->sort);
						$q .= ' ORDER BY ';
						foreach ($list->sort as $s) {
							$direction = $s->ascending ? "ASC" : "DESC";
							foreach ($fields as $f) {
								$f = preg_replace("/toLabel\(([^)]+)\)/", '${1}', $f);
								if (Util::endsWith($f, $s->column)) {
									$q .= $f . ' ' . $direction . ', ';
									break;
								}
							}
						}
						$q = substr($q, 0, strlen($q)-2); // trim the trailing comma
					}
				}
				$q .= ' LIMIT '.$record_limit;
				$q = '('.$q.')';
				$subqs[] = $q;
			}
			
			$query .= implode(', ', $subqs) . " From {$obj} ";

			// save for later
			$_SESSION["{$obj}_query"] = $query;
		} else {
			$query = $_SESSION["{$obj}_query"];
		}

		$query .= "Where Id ='{$person->Id}'";
		
		return $query;
	}
	
	/*
	 * Runs an object's config through 'describe' to gather additional information, like
	 * picklist values, labels, field types, requiredness, etc 
	 */
	public function describeConfig($obj, $config) {
		$this->doGlobalDescribe();
		
		$obj = strtolower($obj);

		$conn = Salesforce::connect();
		$describe = InsightUser::getObjDescribe($obj);
		
		$new_config = array();
		foreach ($config as $ci_field) {
			$found = false;
			
			if (isset($ci_field->type) && $ci_field->type == 'special' && stripos($ci_field->name, "ci__") === 0) {
				$new_config[] = $ci_field;
			} else if (isset($ci_field->type) && $ci_field->type == 'hover') {
				// runs a describe on the referenced object and returns the cleaned list of fields
				$sub_config = $this->describeConfig($ci_field->object, $ci_field->fields);
				$ci_field->fields = $sub_config;
				$new_config[] = $ci_field;
			} else {
				foreach ($describe->fields as $field) {
					// TODO: Normalize field types
					
					if (strtolower($ci_field->name) == strtolower($field->name)) {
						$found = true;
						$ci_field->label = $field->label;
						$ci_field->type = $field->type;
						$ci_field->maxlength = $field->length;

						if ($field->type == 'picklist' || $field->type == 'multipicklist') {
							$ci_field->type = 'picklist'; // force multipicklist to conform; simplify UI
							if (isset($ci_field->location) && ($ci_field->location == 'edit' || $ci_field->location == 'both')) {
								$ci_field->picklistValues = array();
								foreach ($field->picklistValues as $plv) {
									$ci_field->picklistValues[] = array(
										"label" => $plv->value,
										"value" => $plv->value
									);
								}
							}

						} else if (strtolower($field->name) == "recordtypeid") {
							$ci_field->label = "Record Type";
							$ci_field->type = "picklist";
							$ci_field->picklistValues = array();
							foreach ($this->getRecordTypes($obj) as $rt) {
								$ci_field->picklistValues[] = array(
									"label" => $rt->fields->Name,
									"value" => $rt->Id
								);
							}
						}
						
						$ci_field->required = false;
						// if it's not read-only AND can't be null AND isn't a checkbox 
						if ($field->updateable && $field->nillable == false && $field->type != 'boolean') {
							$ci_field->required = true;
						}
						
						break; // break out of describe loop
					}
				}
	
				if ($found) $new_config[] = $ci_field;
			}
		}

		return $new_config;
	}
	
	/*
	 * Loads the default configuration for either Leads or Contacts 
	 */
	public function getObjConfig($obj) {
		$c = new Config($obj, $this->user_id);
		return $c->getConfig();	
	}

	private function getRelatedLists($obj) {
		$c = new Config($obj, $this->user_id);
		return $c->getLists();	
	}
	
	public function hasRelatedList($obj, $list_name) {
		$lists = $this->getRelatedLists($obj);
		return Util::in_iarray($list_name, $lists);
	}
	
	public static $mLayouts = array();
	public function doDescribeLayout($obj) {
		// return from cache if present
		if (array_key_exists($obj, self::$mLayouts)) return self::$mLayouts[$obj];
		
		if (!isset($_SESSION["access_token"])) $this->requestAccessToken();
		$conn = Salesforce::connect();
		$layout = $conn->describeLayout($obj);
		$layouts = array();
		
		//establish record type mappings
		if (isset($layout->recordTypeMappings)) {
			if (!is_array($layout->recordTypeMappings)) 
				$layout->recordTypeMappings = array($layout->recordTypeMappings);
	
			foreach ($layout->recordTypeMappings as $m) {
				if ($m->available) {
					$layouts[$m->recordTypeId] = $m->layoutId;
				}
			}
		}
		
		if (!is_array($layout->layouts)) 
			$layout->layouts = array($layout->layouts);

		foreach ($layout->layouts as $l) {
			if (isset($l->buttonLayoutSection)) unset($l->buttonLayoutSection); // TODO: remove to add button functionality to CI
			if (isset($l->detailLayoutSections)) unset($l->detailLayoutSections);
			if (isset($l->editLayoutSections)) unset($l->editLayoutSections);
			
			// relate the proper layout to the corresponding record type
			foreach ($layouts as $recTypeId => $v) {
				if ($l->id == $v) {
					$layouts[$recTypeId] = $l;
					break;
				}
			}
			
			// Add the 'Description' field to the OpenActivities and ActivityHistories related lists
			// since it can't be added to the page layout in salesforce
			foreach ($l->relatedLists as $rl) {
				if (strtolower($rl->name) == 'activityhistories' || strtolower($rl->name) == 'openactivities') {
					$description = new stdClass();
						$description->field = $rl->sobject . '.Description';
						$description->name = 'Description';
						$description->label = 'Description'; // not localized
						$description->format = null;
					$rl->columns[] = $description;
				}
			}
		}
		
		// cache the result
		self::$mLayouts[$obj] = $layouts;
		return $layouts;
	}
	
	public function getLayout($sObject) {
		$layouts = $this->doDescribeLayout($sObject->type);
		
		$recTypeId = '012000000000000AAA';
		if (isset($sObject->fields->RecordTypeId))
			$recTypeId = $sObject->fields->RecordTypeId;

		// default record type
		if ($recTypeId == '012000000000000AAA') {
			$layoutid = $layouts[$recTypeId];
			foreach ($layouts as $rt => $l) {
				if ($l->id == $layoutid) {
					return $l;
				}
			}
		}
		
		return $layouts[$recTypeId];
	}

	//
	// run global describe to get allowed objects
	//
	public function doGlobalDescribe() {
		if (isset($_SESSION[DESCRIBE_KEY]) && count($_SESSION[DESCRIBE_KEY]) > 0) return;
		if (!isset($_SESSION["access_token"])) {
			$this->requestAccessToken();
		}

		$conn = Salesforce::connect();
		
		// Get the objects that a task can be related to.
		// Then include those objects in the global describe request
		$task = $conn->describeSObject("Task");
		$related_objects = array();
		foreach ($task->fields as $f) {
			if (strtolower($f->name) == 'whatid') {
				foreach ($f->referenceTo as $n) {
					$related_objects[] = strtolower($n);
				}
				break;
			}
		}
		
		$all_objects = array();
		foreach (array_unique(array_merge(InsightUser::$GLOBAL_CONFIG, $related_objects)) as $o) { $all_objects[] = $o; }
		
		// do a describeGlobal to find out which objects the user has access to
		$global_describe = $conn->describeGlobal();
		$global_objects = array();
		foreach ($global_describe->sobjects as $o) $global_objects[] = strtolower($o->name);
		$permitted_objects = array();
		foreach (array_intersect($all_objects, $global_objects) as $o) $permitted_objects[] = $o;

		$sobject_describe = array();
		if (count($permitted_objects) <= 100) {
			$sobject_describe = $conn->describeSObjects($permitted_objects);
		} else {
			// describeSObjects can take up to 100 objects
			// If the org has more, we need to run this multiple times
			$subset = array();
			while (count($permitted_objects) > 0) {
				$subset[] = array_shift($permitted_objects);
				if (count($subset) == 100) {
					$sobject_describe = array_merge($sobject_describe, $conn->describeSObjects($subset));
					$subset = array();
				}
			}
			if (count($subset) > 0) {
				$sobject_describe = array_merge($sobject_describe, $conn->describeSObjects($subset));
			}
		}

		$describe_detail = array();
		$_SESSION[DESCRIBE_KEY] = array();
		$_SESSION[DESCRIBE_KEY]["WHAT_IDS"] = array();
		foreach ($sobject_describe as $d) {
			$desc = new stdClass();
			$desc->fields = array();
			// retain the following fields from the field describe
			$merge = array("name", "label", "length", "picklistValues", "type", "nillable", "createable", "updateable", "defaultedOnCreate");
			foreach ($d->fields as $f) {
				$fld = new stdClass();
				foreach (get_object_vars($f) as $var => $val) {
					if (in_array($var, $merge)) {
						$fld->$var = $val;
					}
				}
				$desc->fields[] = $fld;
			}
			$desc->label = $d->label;
			$desc->isCreateable = $d->createable;
			$_SESSION[DESCRIBE_KEY][strtolower($d->name)] = $desc;
			
			// restructure the whatid data for the front-end
			if (in_array(strtolower($d->name), $related_objects)) {
				$what = new stdClass();
					$what->name = strtolower($d->name);
					$what->label = $d->labelPlural;
				$_SESSION[DESCRIBE_KEY]["WHAT_IDS"][] = $what;
			}
		}
		
		// alphabetize the WHAT_IDS list
		usort($_SESSION[DESCRIBE_KEY]["WHAT_IDS"], function ($a, $b) {
			return ($a->label <= $b->label) ? -1 : 1;
		});
	}
	
	public function getObjDescribe($obj) {
		if (!$this->hasObj($obj)) return null;
		return $_SESSION[DESCRIBE_KEY][strtolower($obj)];
	}
	
	public function hasObj($obj, $hasCreate=false) {
		if (!isset($_SESSION[DESCRIBE_KEY])) {
			// run describe
//			$this->doGlobalDescribe();
		}
		$has = (isset($_SESSION[DESCRIBE_KEY]) && array_key_exists(strtolower($obj), $_SESSION[DESCRIBE_KEY]));
		if ($has && $hasCreate == false) return true;
		
		// $hasCreate=true means, only return true if the user also has the 'createable' permission
		if ($has && $hasCreate && $_SESSION[DESCRIBE_KEY][$obj]->isCreateable) return true;
		
		return false;
	}
	
	public function getAccessibleFields($obj) {
		$obj = strtolower($obj);
		if (array_key_exists($obj, InsightUser::$mSafeFields)) return InsightUser::$mSafeFields[$obj];

		// make sure they have access to the object
		if (!$this->hasObj($obj)) return array();
		
		$safe_fields = array();
		foreach ($_SESSION[DESCRIBE_KEY][$obj]->fields as $field) {
			$safe_fields[] = $field->name;
		}

		// add other safe fields for relationships
		if (in_array('ownerid', $safe_fields)) $safe_fields = array_merge($safe_fields, array('Owner.Name'));
		if (in_array('whoid', $safe_fields)) $safe_fields = array_merge($safe_fields, array('Who.Name', 'Who.Type'));
		if (in_array('whatid', $safe_fields)) $safe_fields = array_merge($safe_fields, array('What.Name', 'What.Type'));

		$safe_fields = array_unique($safe_fields); // remove dups
		InsightUser::$mSafeFields[$obj] = $safe_fields;
		
		return $safe_fields;
	}
	// static store of safe fields per object
	private static $mSafeFields = array();
	
	public function isAccessible($objectName, $fieldName) {
		return Util::in_iarray($fieldName, $this->getAccessibleFields($objectName));
	}
	
	public function hasPersonAccounts() {
		return $this->isAccessible("account", "IsPersonAccount");
	}
	
	public function getPersonRecordType() {
		if ($this->hasPersonAccounts()) {
			$rec_types = $this->getRecordTypes("account");
			foreach ($rec_types as $rt) {
				if ($rt->fields->IsPersonType) return $rt->Id;
			}
		}
		return null;
	}
	
	public function hasRecordTypes($obj) {
		return ($this->hasObj("RecordType") && $this->isAccessible($obj, "RecordTypeId"));
	}
	
	public function getRecordTypes($obj) {
		if ($this->hasRecordTypes($obj)) {
			$personAccount = ($this->hasPersonAccounts() ? ', IsPersonType' : '');
			$query = "Select Id, Name {$personAccount} From RecordType WHERE isActive=true AND SobjectType='{$obj}'";
			$results = Salesforce::sfQueryProxy(Salesforce::connect(), 'query', $query);
			if (isset($results->records)) return $results->records;
		}
		
		return array();
	}
	
	// additional objects and fields that Cirrus Insight uses
	private static $GLOBAL_CONFIG = array("user", "profile", "organization", "recordtype", "event", "task", "lead", "contact", "account", "cirrusinsight__synced_contact__c");
        
	
}

?>